#!/usr/bin/env python3
"""
Edge-TTS 本地 HTTP 服务

使用方法：
1. 安装依赖：pip install edge-tts flask flask-cors
2. 运行服务：python edge_tts_server.py
3. 服务地址：http://localhost:5000

API 接口：
- POST /tts - 文本转语音
  请求体：{
    "text": "要转换的文本",
    "voice": "zh-CN-XiaoxiaoNeural",  # 可选
    "rate": "+0%",                      # 可选，语速
    "volume": "+0%",                    # 可选，音量
    "pitch": "+0Hz"                     # 可选，音调
  }
  响应：MP3 音频数据

- GET /health - 健康检查
  响应：{"status": "ok"}

- GET /voices - 获取可用语音列表
  响应：语音列表

- GET /cache/stats - 获取缓存统计信息
  响应：缓存命中率、文件数量等

- DELETE /cache - 清空缓存
  响应：清理结果
"""

from flask import Flask, request, send_file, jsonify
from flask_cors import CORS
import edge_tts
import asyncio
import io
import logging
from datetime import datetime
import hashlib
import os
from pathlib import Path

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

app = Flask(__name__)
# 启用 CORS，允许浏览器跨域访问
CORS(app)

# ==================== 缓存配置 ====================
# 缓存目录
CACHE_DIR = Path(__file__).parent / 'tts_cache'
CACHE_DIR.mkdir(exist_ok=True)

# 缓存统计
cache_stats = {
    'hits': 0,      # 缓存命中次数
    'misses': 0,    # 缓存未命中次数
    'total': 0      # 总请求次数
}

# 可用的中文语音列表
CHINESE_VOICES = [
    'zh-CN-XiaoxiaoNeural',  # 晓晓 (女声)
    'zh-CN-XiaoyiNeural',    # 晓伊 (女声)
    'zh-CN-YunjianNeural',   # 云健 (男声)
    'zh-CN-YunxiNeural',     # 云希 (男声)
    'zh-CN-YunxiaNeural',    # 云霞 (男声)
    'zh-CN-YunyangNeural',   # 云扬 (男声)
    'zh-CN-liaoning-XiaobeiNeural',  # 晓北 - 辽宁 (女声)
    'zh-CN-shaanxi-XiaoniNeural',    # 晓妮 - 陕西 (女声)
    'zh-HK-HiuGaaiNeural',   # 晓佳 - 粤语 (女声)
    'zh-HK-HiuMaanNeural',   # 晓曼 - 粤语 (女声)
    'zh-HK-WanLungNeural',   # 云龙 - 粤语 (男声)
    'zh-TW-HsiaoChenNeural', # 晓臻 - 台湾 (女声)
    'zh-TW-HsiaoYuNeural',   # 晓雨 - 台湾 (女声)
    'zh-TW-YunJheNeural',    # 云哲 - 台湾 (男声)
]

# ==================== 缓存工具函数 ====================
def get_cache_key(text: str, voice: str, rate: str, volume: str, pitch: str) -> str:
    """
    生成缓存键（基于所有参数的MD5哈希）
    相同的参数组合会生成相同的缓存键
    """
    key_string = f"{text}_{voice}_{rate}_{volume}_{pitch}"
    return hashlib.md5(key_string.encode('utf-8')).hexdigest()

def get_cache_file_path(cache_key: str) -> Path:
    """获取缓存文件路径"""
    return CACHE_DIR / f"{cache_key}.mp3"

def get_cache_info_path(cache_key: str) -> Path:
    """获取缓存信息文件路径（存储文本等元数据）"""
    return CACHE_DIR / f"{cache_key}.info"

def save_cache_info(cache_key: str, text: str, voice: str, rate: str, volume: str, pitch: str):
    """保存缓存信息（方便后续查看）"""
    info_file = get_cache_info_path(cache_key)
    try:
        with open(info_file, 'w', encoding='utf-8') as f:
            f.write(f"文本: {text}\n")
            f.write(f"语音: {voice}\n")
            f.write(f"语速: {rate}\n")
            f.write(f"音量: {volume}\n")
            f.write(f"音调: {pitch}\n")
            f.write(f"创建时间: {datetime.now().isoformat()}\n")
    except Exception as e:
        logger.warning(f"保存缓存信息失败: {e}")

def get_cache_size() -> tuple[int, int]:
    """获取缓存大小（文件数量，总大小MB）"""
    try:
        files = list(CACHE_DIR.glob('*.mp3'))
        total_size = sum(f.stat().st_size for f in files)
        return len(files), total_size / (1024 * 1024)  # 转换为MB
    except Exception as e:
        logger.error(f"获取缓存大小失败: {e}")
        return 0, 0.0

# ==================== API 路由 ====================
@app.route('/tts', methods=['POST'])
def text_to_speech():
    """文本转语音接口（带缓存）"""
    try:
        data = request.get_json()
        if not data:
            return jsonify({'error': 'Invalid request body'}), 400
        
        # 获取参数
        text = data.get('text', '')
        if not text:
            return jsonify({'error': 'Text is required'}), 400
        
        voice = data.get('voice', 'zh-CN-XiaoxiaoNeural')
        rate = data.get('rate', '+0%')
        volume = data.get('volume', '+0%')
        pitch = data.get('pitch', '+0Hz')
        
        # 更新统计
        cache_stats['total'] += 1
        
        # 生成缓存键
        cache_key = get_cache_key(text, voice, rate, volume, pitch)
        cache_file = get_cache_file_path(cache_key)
        
        # 检查缓存是否存在
        if cache_file.exists():
            cache_stats['hits'] += 1
            hit_rate = (cache_stats['hits'] / cache_stats['total']) * 100 if cache_stats['total'] > 0 else 0
            
            logger.info(f'✅ 缓存命中 [{hit_rate:.1f}%] - Text: {text[:50]}...')
            logger.info(f'   缓存键: {cache_key}')
            
            return send_file(
                cache_file,
                mimetype='audio/mpeg',
                as_attachment=False,
                download_name='speech.mp3'
            )
        
        # 缓存未命中
        cache_stats['misses'] += 1
        hit_rate = (cache_stats['hits'] / cache_stats['total']) * 100 if cache_stats['total'] > 0 else 0
        
        logger.info(f'❌ 缓存未命中 [{hit_rate:.1f}%] - 正在请求微软服务器...')
        logger.info(f'   Voice: {voice}, Text: {text[:50]}...')
        
        # 异步生成音频
        async def generate_audio():
            communicate = edge_tts.Communicate(
                text,
                voice,
                rate=rate,
                volume=volume,
                pitch=pitch
            )
            
            audio_data = io.BytesIO()
            async for chunk in communicate.stream():
                if chunk["type"] == "audio":
                    audio_data.write(chunk["data"])
            
            audio_data.seek(0)
            return audio_data
        
        # 运行异步任务
        audio_data = asyncio.run(generate_audio())
        
        # 保存到缓存
        try:
            with open(cache_file, 'wb') as f:
                f.write(audio_data.getvalue())
            
            # 保存缓存信息
            save_cache_info(cache_key, text, voice, rate, volume, pitch)
            
            logger.info(f'💾 已保存到缓存 - Size: {audio_data.getbuffer().nbytes} bytes')
            logger.info(f'   缓存键: {cache_key}')
        except Exception as e:
            logger.warning(f'保存缓存失败（不影响返回）: {e}')
        
        # 重置指针
        audio_data.seek(0)
        
        return send_file(
            audio_data,
            mimetype='audio/mpeg',
            as_attachment=False,
            download_name='speech.mp3'
        )
    
    except Exception as e:
        logger.error(f'TTS Error: {str(e)}', exc_info=True)
        return jsonify({'error': str(e)}), 500

@app.route('/health', methods=['GET'])
def health():
    """健康检查接口"""
    return jsonify({
        'status': 'ok',
        'service': 'Edge-TTS Local Server',
        'version': '1.0.0',
        'timestamp': datetime.now().isoformat()
    })

@app.route('/voices', methods=['GET'])
def get_voices():
    """获取可用语音列表（静态列表）"""
    return jsonify({
        'voices': CHINESE_VOICES,
        'count': len(CHINESE_VOICES),
        'note': '使用 GET /voices/all 获取完整动态列表'
    })

@app.route('/voices/all', methods=['GET'])
def get_all_voices():
    """获取所有可用语音列表（动态从微软服务器获取）"""
    try:
        async def fetch_voices():
            voices = await edge_tts.list_voices()
            # 过滤中文相关语音
            chinese_voices = [
                {
                    'name': v['ShortName'],
                    'display_name': v['FriendlyName'],
                    'locale': v['Locale'],
                    'locale_name': v['LocaleName'],
                    'gender': '女声' if v['Gender'] == 'Female' else '男声',
                    'suggested_codec': v.get('SuggestedCodec', 'audio-24khz-48kbitrate-mono-mp3'),
                }
                for v in voices 
                if v['Locale'].startswith(('zh-CN', 'zh-HK', 'zh-TW'))
            ]
            return chinese_voices
        
        voices = asyncio.run(fetch_voices())
        
        # 按地区分组
        grouped = {
            'zh-CN': {'name': '普通话', 'voices': []},
            'zh-HK': {'name': '粤语', 'voices': []},
            'zh-TW': {'name': '台湾话', 'voices': []}
        }
        
        for voice in voices:
            locale_prefix = voice['locale'].split('-')[0] + '-' + voice['locale'].split('-')[1]
            if locale_prefix in grouped:
                grouped[locale_prefix]['voices'].append(voice)
        
        return jsonify({
            'total': len(voices),
            'grouped': grouped,
            'all_voices': voices
        })
    except Exception as e:
        logger.error(f'获取语音列表失败: {str(e)}', exc_info=True)
        return jsonify({'error': str(e)}), 500

@app.route('/cache/stats', methods=['GET'])
def get_cache_stats():
    """获取缓存统计信息"""
    file_count, total_size = get_cache_size()
    hit_rate = (cache_stats['hits'] / cache_stats['total'] * 100) if cache_stats['total'] > 0 else 0
    
    return jsonify({
        'statistics': {
            'total_requests': cache_stats['total'],
            'cache_hits': cache_stats['hits'],
            'cache_misses': cache_stats['misses'],
            'hit_rate': f"{hit_rate:.2f}%"
        },
        'cache': {
            'file_count': file_count,
            'total_size_mb': f"{total_size:.2f}",
            'cache_directory': str(CACHE_DIR)
        }
    })

@app.route('/cache', methods=['DELETE'])
def clear_cache():
    """清空缓存"""
    try:
        deleted_count = 0
        deleted_size = 0
        
        # 删除所有缓存文件
        for file in CACHE_DIR.glob('*'):
            if file.is_file():
                deleted_size += file.stat().st_size
                file.unlink()
                deleted_count += 1
        
        logger.info(f'缓存已清空 - 删除 {deleted_count} 个文件，释放 {deleted_size / (1024 * 1024):.2f} MB')
        
        return jsonify({
            'success': True,
            'deleted_files': deleted_count,
            'freed_space_mb': f"{deleted_size / (1024 * 1024):.2f}"
        })
    except Exception as e:
        logger.error(f'清空缓存失败: {str(e)}', exc_info=True)
        return jsonify({'error': str(e)}), 500

@app.route('/', methods=['GET'])
def index():
    """根路径"""
    file_count, total_size = get_cache_size()
    
    return jsonify({
        'service': 'Edge-TTS Local Server (with Cache)',
        'version': '1.1.0',
        'endpoints': {
            'POST /tts': 'Text to Speech (with cache)',
            'GET /health': 'Health Check',
            'GET /voices': 'List Chinese Voices (static)',
            'GET /voices/all': 'List All Chinese Voices (dynamic from Microsoft)',
            'GET /cache/stats': 'Cache Statistics',
            'DELETE /cache': 'Clear Cache'
        },
        'cache_info': {
            'enabled': True,
            'file_count': file_count,
            'size_mb': f"{total_size:.2f}"
        },
        'docs': 'https://github.com/rany2/edge-tts'
    })

def main():
    """主函数"""
    file_count, total_size = get_cache_size()
    
    logger.info('=' * 60)
    logger.info('Edge-TTS 本地服务启动中 (带缓存功能)')
    logger.info('=' * 60)
    logger.info('服务地址: http://localhost:5000')
    logger.info('健康检查: http://localhost:5000/health')
    logger.info('可用语音: http://localhost:5000/voices')
    logger.info('缓存统计: http://localhost:5000/cache/stats')
    logger.info('=' * 60)
    logger.info(f'缓存目录: {CACHE_DIR}')
    logger.info(f'已缓存文件: {file_count} 个')
    logger.info(f'缓存大小: {total_size:.2f} MB')
    logger.info('=' * 60)
    logger.info('按 Ctrl+C 停止服务')
    logger.info('=' * 60)
    
    try:
        # 启动 Flask 服务
        app.run(
            host='0.0.0.0',  # 允许外部访问
            port=5000,
            debug=False,
            threaded=True
        )
    except KeyboardInterrupt:
        logger.info('\n服务已停止')
    except Exception as e:
        logger.error(f'服务启动失败: {str(e)}', exc_info=True)

if __name__ == '__main__':
    main()

